Make SDK and EventSubscribe shutdown idempotent#956
Conversation
* Initial plan * Remove fisco-bcos.org URL, replace with GitHub repo URL in build.gradle Co-authored-by: kyonRay <32325790+kyonRay@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: kyonRay <32325790+kyonRay@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR hardens lifecycle management around JNI-backed SDK handles to prevent double-stop/double-destroy sequences (which can crash the JVM), and clarifies EventSubscribe ownership so it doesn’t tear down shared native state out of order.
Changes:
- Make
ClientImpl.start/stop/destroysynchronized and stateful to ensure shutdown is idempotent. - Introduce an ownership model in
EventSubscribe/EventSubscribeImpso instances created viabuild(group, config)own and manage their internal client, whilebuild(client)borrows without stopping/destroying it. - Add unit tests for repeated
ClientImplandEventSubscribeImpshutdown calls; update POM metadata URL.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/main/java/org/fisco/bcos/sdk/v3/client/ClientImpl.java |
Adds lifecycle state + synchronization to make start/stop/destroy idempotent. |
src/main/java/org/fisco/bcos/sdk/v3/eventsub/EventSubscribe.java |
Marks group-based build(...) as owning its internally created client. |
src/main/java/org/fisco/bcos/sdk/v3/eventsub/EventSubscribeImp.java |
Tracks ownership and makes stop/destroy idempotent; unsubscribes before teardown. |
src/test/java/org/fisco/bcos/sdk/v3/client/ClientImplTest.java |
Adds regression test for repeated stop/destroy (needs adjustment to avoid native teardown). |
src/test/java/org/fisco/bcos/sdk/v3/eventsub/EventSubscribeImpTest.java |
Adds regression tests for shared vs owned client shutdown (needs adjustment to avoid JNI init in unit tests). |
build.gradle |
Updates published project URL to the GitHub repository. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @Override | ||
| public void start() { | ||
| eventSubJniObj.start(); | ||
| public synchronized void start() { | ||
| if (destroyed) { | ||
| return; | ||
| } | ||
| if (ownsClient) { | ||
| ownerClient.start(); | ||
| } | ||
| stopped = false; | ||
| } |
| private static void setField(Object target, String fieldName, Object value) throws Exception { | ||
| Field field = target.getClass().getDeclaredField(fieldName); | ||
| field.setAccessible(true); | ||
| field.set(target, value); | ||
| } | ||
| } |
| client.stop(); | ||
| client.stop(); | ||
| client.destroy(); | ||
| client.destroy(); | ||
|
|
| Client client = mock(Client.class); | ||
| when(client.getGroup()).thenReturn("group0"); | ||
| when(client.getNativePointer()).thenReturn(0L); | ||
|
|
||
| EventSubscribeImp eventSubscribe = new EventSubscribeImp(client, null); | ||
| EventSubJniObj eventSubJniObj = mock(EventSubJniObj.class); | ||
| when(eventSubJniObj.getAllSubscribedEvents()) | ||
| .thenReturn(new HashSet<>(Arrays.asList("event-a", "event-b"))); | ||
| setField(eventSubscribe, "eventSubJniObj", eventSubJniObj); |
| Client client = mock(Client.class); | ||
| when(client.getGroup()).thenReturn("group0"); | ||
| when(client.getNativePointer()).thenReturn(0L); | ||
|
|
||
| EventSubscribeImp eventSubscribe = new EventSubscribeImp(client, null, true); | ||
| EventSubJniObj eventSubJniObj = mock(EventSubJniObj.class); | ||
| when(eventSubJniObj.getAllSubscribedEvents()) | ||
| .thenReturn(new HashSet<>(Arrays.asList("event-a"))); | ||
| setField(eventSubscribe, "eventSubJniObj", eventSubJniObj); |
Review summary — core design is correct, a few things to fix before merge✅ The core fix is sound. I verified that
A few items to address, @copilot: 1.
|
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## release-3.9.0 #956 +/- ##
================================================
Coverage ? 52.20%
Complexity ? 4056
================================================
Files ? 430
Lines ? 17628
Branches ? 1969
================================================
Hits ? 9203
Misses ? 7684
Partials ? 741
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|



Stopping or destroying
Client/EventSubscribecould re-enter native shutdown on the same JNI-backed handle and crash the JVM inbcos_sdk_stop. This change tightens Java-side lifecycle handling so repeated shutdown paths do not double-stop or tear down shared native state out of order.Client lifecycle hardening
ClientImpl.start(),stop(), anddestroy()synchronized and stateful.stop()/destroy()calls after the first effective shutdown.RpcJniObj.stop()/BcosSDKJniObj.destroy(...)on the same client handle.Event subscription ownership model
EventSubscribe.build(group, config)→ owns the internally created clientEventSubscribe.build(client)→ borrows the caller-provided clientEventSubscribeinstance owns it.Safer EventSubscribe shutdown
EventSubscribeImp.stop()/destroy()idempotent.Focused regression coverage
ClientImpl.stop()/destroy().EventSubscribeshutdown behavior.Example of the intended behavior: